﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using Creventive.Wpf.ShellFactory.Services;
using Creventive.Wpf.ShellFactory.Utility;

namespace Creventive.Wpf.ShellFactory.Modules
{
    /// <summary>
    ///   Defines a catalog where all modules are loaded.
    /// </summary>
    public class ApplicationModuleService : Sealable, IApplicationModuleService
    {
        private List<ModuleInfo> modules;
        private ModuleInfo shellModuleInfo;
        private bool isLoaded;

        /// <summary>
        ///   Initializes a new instance of the <see cref = "ApplicationModuleService" /> class.
        /// </summary>
        public ApplicationModuleService()
        {
            this.modules = new List<ModuleInfo>();
        }

        /// <summary>
        ///   Occurs when the modules in this catalog have been loaded.
        /// </summary>
        public event EventHandler Loaded;

        /// <summary>
        ///   Gets the application modules. Will not be available until the modules are loaded.
        /// </summary>
        /// <value>The application modules.</value>
        public IEnumerable<ApplicationModule> ApplicationModules
        {
            get { return this.Where(m => m.ApplicationModule != null).Select(m => m.ApplicationModule); }
        }

        /// <summary>
        ///   Gets the number of registered modules.
        /// </summary>
        /// <value>The count.</value>
        public int Count
        {
            get { return this.modules.Count; }
        }

        /// <summary>
        ///   Gets a value indicating whether the modules in this catalog have been loaded.
        /// </summary>
        /// <value>
        ///   <c>true</c> if the modules have been loaded; otherwise, <c>false</c>.
        /// </value>
        public bool IsLoaded
        {
            get { return this.isLoaded; }
        }

        /// <summary>
        ///   Gets the <see cref = "Creventive.Wpf.ShellFactory.Modules.ModuleInfo" /> for the specified assembly name.
        /// </summary>
        /// <value></value>
        public ModuleInfo this[AssemblyName assemblyName]
        {
            get
            {
                if (assemblyName == null)
                    throw new ArgumentNullException("assemblyName");
                return this.modules.SingleOrDefault(m => m.AssemblyName.ToString() == assemblyName.ToString());
            }
        }

        /// <summary>
        ///   Gets the <see cref = "Creventive.Wpf.ShellFactory.Modules.ModuleInfo" /> at the specified index.
        /// </summary>
        /// <value></value>
        public ModuleInfo this[int index]
        {
            get { return this.modules[index]; }
        }

        /// <summary>
        ///   Gets the shell module info. Will not be available until the modules are loaded.
        /// </summary>
        /// <value>The shell module info.</value>
        public ModuleInfo ShellModuleInfo
        {
            get
            {
                if (!this.isLoaded)
                    throw new InvalidOperationException("Shell module is not available until modules have been loaded");
                return this.shellModuleInfo;
            }
        }

        /// <summary>
        /// Retrieves module information for the given assembly, and describing the assembly as the given module type.
        /// </summary>
        /// <param name="assemblyName">Name of the assembly.</param>
        /// <param name="moduleType">Type of the module.</param>
        /// <returns></returns>
        protected virtual ModuleInfo CreateModuleInfo(AssemblyName assemblyName, ModuleType moduleType)
        {
            switch (moduleType)
            {
                case ModuleType.Static:
                    return new StaticModuleInfo(assemblyName);
                case ModuleType.Shell:
                    return new ShellModuleInfo(assemblyName);
                case ModuleType.Dynamic:
                    throw new NotImplementedException();

                default:
                    throw new ArgumentNullException("moduleType");
            }
        }

        /// <summary>
        ///   Loads the module.
        /// </summary>
        /// <param name = "moduleInfo">The module info.</param>
        /// <param name = "serviceContainer">The service container.</param>
        /// <returns></returns>
        protected virtual ApplicationModule LoadModule(ModuleInfo moduleInfo, IServiceContainer serviceContainer)
        {
            return ApplicationModule.Load(moduleInfo, serviceContainer);
        }

        /// <summary>
        ///   Called when all modules have been loaded.
        /// </summary>
        protected virtual void OnLoaded()
        {
            this.isLoaded = true;

            // Notify all module controllers that all modules have been loaded.
            this.ApplicationModules.ForEach(m => m.NotifyModulesLoaded());

            var handler = this.Loaded;
            if (handler != null)
                handler.Invoke(this, EventArgs.Empty);
        }

        /// <summary>
        ///   Gets the enumerator.
        /// </summary>
        /// <returns></returns>
        public IEnumerator<ModuleInfo> GetEnumerator()
        {
            return this.modules.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        internal void RegisterShellModule(AssemblyName shellAssemblyName, ApplicationModule shellModule)
        {
            if (this.shellModuleInfo != null)
                throw new InvalidOperationException("Duplicate Shell Module Registration");
            var moduleInfo = this.CreateModuleInfo(shellAssemblyName, ModuleType.Shell);
            this.modules.Add(moduleInfo);
            if (shellModule != null)
                moduleInfo.OnBeforeLoaded(shellModule);
            this.shellModuleInfo = moduleInfo;
        }

        internal void RegisterStaticModule(Uri source)
        {
            ApplicationModule staticModule;
            try
            {
                staticModule = Application.LoadComponent(source) as ApplicationModule;
                if (staticModule == null)
                    throw new InvalidOperationException("Invalid Module URI " + source);
            }
            catch (Exception e)
            {
                throw new InvalidOperationException("Invalid Module URI " + source, e);
            }

            var moduleInfo = this.CreateModuleInfo(staticModule.GetType().Assembly.GetName(), ModuleType.Static);
            moduleInfo.OnBeforeLoaded(staticModule);
            this.modules.Add(moduleInfo);
        }

        internal void LoadModules()
        {
            var orderedModuleInfos = this.OrderBy(m => m.ModuleType);
            foreach (var moduleInfo in orderedModuleInfos)
            {
                var applicationModule = this.LoadModule(moduleInfo, ServiceContainer.Default);
                moduleInfo.OnBeforeLoaded(applicationModule);
            }

            this.OnLoaded();
        }
    }
}